package com.hse.application;

import com.meshnetics.gb.event.Event;
import com.meshnetics.gb.event.EventListener;
import com.meshnetics.gb.stack.core.event.EventDispatcher;
import com.meshnetics.gb.stack.core.event.StackConfirm;
import com.meshnetics.gb.stack.core.event.StackRequest;
import com.meshnetics.gb.stack.core.event.StackResponse;
import com.meshnetics.gb.stack.core.layer.AbstractStackLayer;
import com.meshnetics.gb.stack.core.layer.SimpleIDGenerator;
import com.meshnetics.gb.stack.mac.RadioMACStackLayer;
import com.meshnetics.gb.stack.mac.primitives.*;
import com.meshnetics.gb.stack.mac.type.*;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;

public class ApplicationMacStackLayer extends AbstractStackLayer {
    private static final Log log = LogFactory.getLog(ApplicationMacStackLayer.class);

    private static final long TIME_OUT = 5000;
    private RadioMACStackLayer radioMACStackLayer;
    private final RequestContext lastRequestContext = new RequestContext();
    private final Queue<MacStackLayerClient> clients = new LinkedBlockingQueue<MacStackLayerClient>();
    private final List<DataPacket> dataPackets = new ArrayList<DataPacket>();
    private final Integer BEACON_PAYLOAD_LENGTH_VALUE = (Integer) 15;

    public static final int LOGICAL_CHANNEL = 16;
    public static final int BEACON_ORDER = 15;
    public static final int SUPERFRAME_ORDER = 15;
    public static final Short COORDINATOR_SHORT_ADDRESS = (short) 0;

    public ApplicationMacStackLayer(String portName) throws InstantiationException {
        super("MacApplication", new AppInformationBase(), new SimpleIDGenerator());
        try {
            String connection = portName + "?baudrate=115200&parity=none&stopbits=1&databits=8&flowcontrol=hard";
            System.setProperty(RadioMACStackLayer.SYSTEM_PROPERTY_CONNECTION, connection);
            radioMACStackLayer = new RadioMACStackLayer("Mac", new MacInformationBase());
            radioMACStackLayer.setUpperLayer(this);
            radioMACStackLayer.start();
            //radioMACStackLayer.setStack(this);//To handle global events like errors
        } catch (Exception ex) {
            throw new InstantiationException(ex.toString());
        }
    }


    private EventListener listener = new EventListener() {
        public void handle(Event evt) {
            log.info(evt.toString());
            if (evt instanceof MACAssociateIndication) {
                final MACAssociateIndication assInd = (MACAssociateIndication) evt;
                final Short shortAddress = generateClientAddress();
                MACAssociateResponse assResp = new MACAssociateResponse(
                        assInd.getDeviceAddress(), shortAddress,
                        AssociationStatus.SUCCESSFUL, Boolean.FALSE);
                executeResponse(assResp);
                synchronized (clients) {
                    clients.offer(new MacStackLayerClient(assInd.getDeviceAddress(), shortAddress));
                    clients.notify();
                }
            } else if (evt instanceof MACDataIndication) {
                final MACDataIndication macDataIndication = (MACDataIndication) evt;
                DataPacket dataPacket = new DataPacket(
                        macDataIndication.getSrcAddr().getPanId(),
                        macDataIndication.getSrcAddr().getShortAddress(),
                        macDataIndication.getDstAddr().getShortAddress(),
                        macDataIndication.getMsdu());
                synchronized (dataPackets) {
                    dataPackets.add(dataPacket);
                    dataPackets.notifyAll();
                }
            } else if (evt instanceof MACReplyOnCommand) {
                final MACReplyOnCommand macReplyOnCommand = (MACReplyOnCommand) evt;
                if (macReplyOnCommand.isReplyOn(lastRequestContext.getRequest())) {
                    lastRequestContext.setResponse((StackConfirm) evt);
                    synchronized (lastRequestContext) {
                        lastRequestContext.notifyAll();
                    }
                } else
                    log.info("Unknow reply command: " + evt.toString());
            }
        }
    };

    public void executeResponse(StackResponse req) {
        radioMACStackLayer.getEventListener().handle(req);
    }

    public StackConfirm execute(StackRequest req, long timeout) throws ApplicationLayerException {
        if (!(req instanceof MACReplyAwaited))
            throw new ApplicationLayerException("request is not of type MACReplyAwaited");

        req.setID(this.getIDGenerator().generateID());
        lastRequestContext.setRequest(req);
        radioMACStackLayer.getEventListener().handle(req);
        try {
            synchronized (lastRequestContext) {
                lastRequestContext.wait(timeout);
            }
        } catch (Exception ex) {
            throw new ApplicationLayerException(ex);
        }
        return (StackConfirm) lastRequestContext.getResponse();
    }

    public StackConfirm execute(StackRequest req) throws ApplicationLayerException {
        return execute(req, TIME_OUT);
    }

    public void init(Long macAddressValue) throws ApplicationLayerException {
        MACResetRequest macResetRequest = new MACResetRequest(true);
        MACResetConfirm macResetConfirm = (MACResetConfirm) execute(macResetRequest);
        if (macResetConfirm.getStatus() != MACStatus.SUCCESS)
            //TODO: ApplicationLayerException replace 'MACResetRequest' word
            throw new ApplicationLayerException("MACResetRequest: " + macResetConfirm.toString());

        MACSetRequest macSetRequest = new MACSetRequest(MacSetConstants.IEEE_ADDRESS, macAddressValue);
        MACSetConfirm macSetConfirm = (MACSetConfirm) execute(macSetRequest);
        if (macSetConfirm.getStatus() != MACStatus.SUCCESS)
            throw new ApplicationLayerException("MACResetRequest: " + macSetConfirm.toString());
    }

    public PANDescriptor[] scan() throws ApplicationLayerException {
        MACScanRequest macScanRequest = new MACScanRequest(ScanType.ACTIVE_SCAN, new ChannelMask(0x10000), 5);
        MACScanConfirm macScanConfirm = (MACScanConfirm) execute(macScanRequest);
        if (macScanConfirm.getStatus() == MACStatus.NO_BEACON)
            return new PANDescriptor[0];
        if (macScanConfirm.getStatus() != MACStatus.SUCCESS)
            throw new ApplicationLayerException("MACResetRequest: " + macScanConfirm.toString());

        return macScanConfirm.getPanDescriptorList().listPANDescriptors();
    }

    public void createNetwork(Short panId) throws ApplicationLayerException {
        MACSetRequest macSetPanId = new MACSetRequest(MacSetConstants.PANID, panId);
        MACSetConfirm macSetPanIdConfirm = (MACSetConfirm) execute(macSetPanId);
        if (macSetPanIdConfirm.getStatus() != MACStatus.SUCCESS)
            throw new ApplicationLayerException("MACResetRequest: " + macSetPanIdConfirm.toString());

        MACSetRequest macSetCoordShortAddress = new MACSetRequest(MacSetConstants.SHORT_ADDR, COORDINATOR_SHORT_ADDRESS);
        MACSetConfirm macSetCoordShortAddressConfirm = (MACSetConfirm) execute(macSetCoordShortAddress);
        if (macSetCoordShortAddressConfirm.getStatus() != MACStatus.SUCCESS)
            throw new ApplicationLayerException("MACResetRequest: " + macSetCoordShortAddressConfirm.toString());

        MACSetRequest macSetBeaconPayloadL = new MACSetRequest(MacSetConstants.BEACON_PAYLOAD_LENGTH, BEACON_PAYLOAD_LENGTH_VALUE);
        MACSetConfirm macSetBeaconPayloadLConfirm = (MACSetConfirm) execute(macSetBeaconPayloadL);
        if (macSetBeaconPayloadLConfirm.getStatus() != MACStatus.SUCCESS)
            throw new ApplicationLayerException("MACResetRequest: " + macSetBeaconPayloadLConfirm.toString());

        NodeDescriptor nodeDescriptor = new NodeDescriptor(0, 0, true, 0, true, 0x0000000000000001L, 0);
        MACSetRequest macSetBeaconPayload = new MACSetRequest(MacSetConstants.BEACON_PAYLOAD, nodeDescriptor.toBytes());
        MACSetConfirm macSetBeaconPayloadConfirm = (MACSetConfirm) execute(macSetBeaconPayload);
        if (macSetBeaconPayloadConfirm.getStatus() != MACStatus.SUCCESS)
            throw new ApplicationLayerException("MACResetRequest: " + macSetBeaconPayloadConfirm.toString());

        MACSetRequest macSetRxOnWhenIdle = new MACSetRequest(MacSetConstants.RX_ON_WHEN_IDLE, Boolean.TRUE);
        MACSetConfirm macSetRxOnWhenIdleConfirm = (MACSetConfirm) execute(macSetRxOnWhenIdle);
        if (macSetRxOnWhenIdleConfirm.getStatus() != MACStatus.SUCCESS)
            throw new ApplicationLayerException("MACResetRequest: " + macSetRxOnWhenIdleConfirm.toString());

        MACRxEnableRequest rxEnable = new MACRxEnableRequest(true, 1, 1);
        MACRxEnableConfirm rxEnableConfirm = (MACRxEnableConfirm) execute(rxEnable);
        if (rxEnableConfirm.getStatus() != MACStatus.SUCCESS)
            throw new ApplicationLayerException("MACResetRequest: " + rxEnableConfirm.toString());


        MACStartRequest startRequest = new MACStartRequest(panId, LOGICAL_CHANNEL,
                BEACON_ORDER, SUPERFRAME_ORDER, true, false, false, false);
        MACStartConfirm startConfirm = (MACStartConfirm) execute(startRequest);
        if (startConfirm.getStatus() != MACStatus.SUCCESS)
            throw new ApplicationLayerException("MACResetRequest: " + startConfirm.toString());

        MACSetRequest associationPermit = new MACSetRequest(MacSetConstants.ASSOCIATION_PERMIT, Boolean.TRUE);
        MACSetConfirm associationPermitConfirm = (MACSetConfirm) execute(associationPermit);
        if (associationPermitConfirm.getStatus() != MACStatus.SUCCESS)
            throw new ApplicationLayerException("MACResetRequest: " + associationPermitConfirm.toString());

        log.info("Network was created with panId: " + panId);
    }

    public Short connect(Short panId) throws ApplicationLayerException {
        DataAddress dataAddress = new DataAddress(panId, COORDINATOR_SHORT_ADDRESS);
        MACAssociateRequest assReq = new MACAssociateRequest(LOGICAL_CHANNEL, dataAddress,
                new Capability(false, false, false, true, false, true), false);
        MACAssociateConfirm assConf = (MACAssociateConfirm) execute(assReq);
        if (assConf.getStatus() != MACStatus.SUCCESS)
            throw new ApplicationLayerException("MACResetRequest: " + assConf.toString());
        Short shortAddress = assConf.getAssocShortAddr();

        MACSetRequest setPanIdReq = new MACSetRequest(MacSetConstants.PANID, panId);
        MACSetConfirm setPanIdConf = (MACSetConfirm) execute(setPanIdReq);
        if (setPanIdConf.getStatus() != MACStatus.SUCCESS)
            throw new ApplicationLayerException("MACResetRequest: " + setPanIdConf.toString());

        MACSetRequest setShortAddrReq = new MACSetRequest(MacSetConstants.SHORT_ADDR, shortAddress);
        MACSetConfirm setShortAddrConf = (MACSetConfirm) execute(setShortAddrReq);
        if (setShortAddrConf.getStatus() != MACStatus.SUCCESS)
            throw new ApplicationLayerException("MACResetRequest: " + setShortAddrConf.toString());

        MACSetRequest macSetRxOnWhenIdle = new MACSetRequest(MacSetConstants.RX_ON_WHEN_IDLE, Boolean.TRUE);
        MACSetConfirm macSetRxOnWhenIdleConfirm = (MACSetConfirm) execute(macSetRxOnWhenIdle);
        if (macSetRxOnWhenIdleConfirm.getStatus() != MACStatus.SUCCESS)
            throw new ApplicationLayerException("MACResetRequest: " + macSetRxOnWhenIdleConfirm.toString());

        return shortAddress;
    }

    public boolean canAcceptEndDevice() {
        synchronized (clients) {
            return clients.size() > 0;
        }
    }

    public MacStackLayerClient acceptEndDevice() throws ApplicationLayerException {
        return acceptEndDevice(0);
    }

    public MacStackLayerClient acceptEndDevice(long timeout) throws ApplicationLayerException {
        synchronized (clients) {
            if (clients.size() > 0)
                return clients.poll();
        }

        try {
            synchronized (clients) {
                clients.wait(timeout);
            }
        } catch (InterruptedException e) {
            throw new ApplicationLayerException(e);
        }
        if (clients.size() == 0)
            throw new ApplicationLayerException("Time out: there is no connected endDevices");
        return clients.poll();
    }

    public void send(short panId, short srcAddress, short dstAddress, byte[] data) throws ApplicationLayerException {
        MACDataRequest macDataRequest = new MACDataRequest(new DataAddress(panId, srcAddress),
                new DataAddress(panId, dstAddress), data, 55, new TxOptions(false, false, false, false));
        MACDataConfirm dataConf = (MACDataConfirm) execute(macDataRequest);
        if (dataConf.getStatus() != MACStatus.SUCCESS)
            throw new ApplicationLayerException("MACResetRequest: " + dataConf.toString());
    }

    public byte[] recv(short panId, short srcAddress, short dstAddress) {
        synchronized (dataPackets) {
            while (true) {
                for (DataPacket dataPacket : dataPackets)
                    if (dataPacket.getPanId() == panId && dataPacket.getRcvAddress() == srcAddress &&
                            dataPacket.getDstAddress() == dstAddress) {
                        dataPackets.remove(dataPacket);
                        return dataPacket.getData();
                    }
                try {
                    dataPackets.wait(0);
                } catch (InterruptedException e) {
                }
            }
        }
    }

    public boolean canRead(short panId, short srcAddress, short dstAddress) {
        synchronized (dataPackets) {
            for (DataPacket dataPacket : dataPackets)
                if (dataPacket.getPanId() == panId && dataPacket.getRcvAddress() == srcAddress &&
                        dataPacket.getDstAddress() == dstAddress) {
                    return true;
                }
            return false;
        }
    }

    public EventListener getEventListener() {
        return listener;
    }

    public void start() {
    }

    public void stop() {
        radioMACStackLayer.stop();
    }

    public EventDispatcher getEventDispatcher() {
        return null;
    }

    private Short generateClientAddress() {
        return (short) this.getIDGenerator().generateID();
    }
}
